/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.apache.tomcat.util.net.jsse;

import org.apache.tomcat.util.compat.JreCompat;
import org.apache.tomcat.util.compat.JreVendor;
import org.apache.tomcat.util.file.ConfigFileLoader;
import org.apache.tomcat.util.net.AbstractEndpoint;
import org.apache.tomcat.util.net.Constants;
import org.apache.tomcat.util.net.SSLUtil;
import org.apache.tomcat.util.net.ServerSocketFactory;
import org.apache.tomcat.util.res.StringManager;

import javax.net.ssl.*;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.*;
import java.util.*;

/**
 * SSL server socket factory. It <b>requires</b> a valid RSA key and
 * JSSE.<br/>
 * keytool -genkey -alias tomcat -keyalg RSA</br>
 * Use "changeit" as password (this is the default we use).
 *
 * @author Harish Prabandham
 * @author Costin Manolache
 * @author Stefan Freyr Stefansson
 * @author EKR -- renamed to JSSESocketFactory
 * @author Jan Luehe
 * @author Bill Barker
 */
public class JSSESocketFactory implements ServerSocketFactory, SSLUtil {

	public static final String DEFAULT_KEY_PASS = "changeit";
	private static final org.apache.juli.logging.Log log =
			org.apache.juli.logging.LogFactory.getLog(JSSESocketFactory.class);
	private static final StringManager sm =
			StringManager.getManager("org.apache.tomcat.util.net.jsse.res");
	// Defaults - made public where re-used
	private static final String defaultProtocol = Constants.SSL_PROTO_TLS;
	private static final String defaultKeystoreType = "JKS";
	private static final String defaultKeystoreFile
			= System.getProperty("user.home") + "/.keystore";
	private static final int defaultSessionCacheSize = 0;
	private static final int defaultSessionTimeout = 86400;
	private static final String ALLOW_ALL_SUPPORTED_CIPHERS = "ALL";
	private final boolean rfc5746Supported;
	private final String[] defaultServerProtocols;
	private final String[] defaultServerCipherSuites;
	protected SSLServerSocketFactory sslProxy = null;
	protected String[] enabledCiphers;
	protected String[] enabledProtocols;
	protected boolean allowUnsafeLegacyRenegotiation = false;
	/**
	 * Flag to state that we require client authentication.
	 */
	protected boolean requireClientAuth = false;
	/**
	 * Flag to state that we would like client authentication.
	 */
	protected boolean wantClientAuth = false;
	private AbstractEndpoint<?> endpoint;

	public JSSESocketFactory(AbstractEndpoint<?> endpoint) {
		this.endpoint = endpoint;

		String sslProtocol = endpoint.getSslProtocol();
		if (sslProtocol == null) {
			sslProtocol = defaultProtocol;
		}

		SSLContext context;
		try {
			context = SSLContext.getInstance(sslProtocol);
			context.init(null, null, null);
		} catch (NoSuchAlgorithmException e) {
			// This is fatal for the connector so throw an exception to prevent
			// it from starting
			throw new IllegalArgumentException(e);
		} catch (KeyManagementException e) {
			// This is fatal for the connector so throw an exception to prevent
			// it from starting
			throw new IllegalArgumentException(e);
		}

		// Supported cipher suites aren't accessible directly from the
		// SSLContext so use the SSL server socket factory
		SSLServerSocketFactory ssf = context.getServerSocketFactory();
		String supportedCiphers[] = ssf.getSupportedCipherSuites();
		boolean found = false;
		for (String cipher : supportedCiphers) {
			if ("TLS_EMPTY_RENEGOTIATION_INFO_SCSV".equals(cipher)) {
				found = true;
				break;
			}
		}
		rfc5746Supported = found;

		// There is no standard way to determine the default protocols and
		// cipher suites so create a server socket to see what the defaults are
		SSLServerSocket socket;
		try {
			socket = (SSLServerSocket) ssf.createServerSocket();
		} catch (IOException e) {
			// This is very likely to be fatal but there is a slim chance that
			// the JSSE implementation just doesn't like creating unbound
			// sockets so allow the code to proceed.
			defaultServerCipherSuites = new String[0];
			defaultServerProtocols = new String[0];
			log.warn(sm.getString("jsse.noDefaultCiphers", endpoint.getName()));
			log.warn(sm.getString("jsse.noDefaultProtocols", endpoint.getName()));
			return;
		}

		try {
			// Many of the default ciphers supported by older JRE versions are
			// now considered insecure. This code attempts to filter them out
			List<String> filteredCiphers = new ArrayList<String>();
			for (String cipher : socket.getEnabledCipherSuites()) {
				// Remove export ciphers - FREAK
				if (cipher.toUpperCase(Locale.ENGLISH).contains("EXP")) {
					log.debug(sm.getString("jsse.excludeDefaultCipher", cipher));
					continue;
				}
				// Remove DES ciphers
				if (cipher.toUpperCase(Locale.ENGLISH).contains("_DES_")) {
					log.debug(sm.getString("jsse.excludeDefaultCipher", cipher));
					continue;
				}
				// Remove RC4 ciphers
				if (cipher.toUpperCase(Locale.ENGLISH).contains("_RC4_")) {
					log.debug(sm.getString("jsse.excludeDefaultCipher", cipher));
					continue;
				}
				// Remove DHE ciphers unless running on Java 8 or above
				if (!JreCompat.isJre8Available() &&
						cipher.toUpperCase(Locale.ENGLISH).contains("_DHE_")) {
					log.debug(sm.getString("jsse.excludeDefaultCipher", cipher));
					continue;
				}
				// Remove kRSA ciphers when running on Java 7 or above. Can't
				// remove them for Java 6 since they are likely to be the only
				// ones left
				if (JreCompat.isJre7Available() &&
						(cipher.toUpperCase(Locale.ENGLISH).startsWith("TLS_RSA_") ||
								cipher.toUpperCase(Locale.ENGLISH).startsWith("SSL_RSA_"))) {
					log.debug(sm.getString("jsse.excludeDefaultCipher", cipher));
					continue;
				}
				filteredCiphers.add(cipher);
			}

			defaultServerCipherSuites = filteredCiphers.toArray(new String[filteredCiphers.size()]);
			if (defaultServerCipherSuites.length == 0) {
				log.warn(sm.getString("jsse.noDefaultCiphers", endpoint.getName()));
			}

			// Filter out all the SSL protocols (SSLv2 and SSLv3) from the defaults
			// since they are no longer considered secure
			List<String> filteredProtocols = new ArrayList<String>();
			for (String protocol : socket.getEnabledProtocols()) {
				if (protocol.toUpperCase(Locale.ENGLISH).contains("SSL")) {
					log.debug(sm.getString("jsse.excludeDefaultProtocol", protocol));
					continue;
				}
				filteredProtocols.add(protocol);
			}
			defaultServerProtocols = filteredProtocols.toArray(new String[filteredProtocols.size()]);
			if (defaultServerProtocols.length == 0) {
				log.warn(sm.getString("jsse.noDefaultProtocols", endpoint.getName()));
			}
		} finally {
			try {
				socket.close();
			} catch (IOException e) {
				log.warn(sm.getString("jsse.exceptionOnClose"), e);
			}
		}
	}

	@Override
	public ServerSocket createSocket(int port)
			throws IOException {
		init();
		ServerSocket socket = sslProxy.createServerSocket(port);
		initServerSocket(socket);
		return socket;
	}

	@Override
	public ServerSocket createSocket(int port, int backlog)
			throws IOException {
		init();
		ServerSocket socket = sslProxy.createServerSocket(port, backlog);
		initServerSocket(socket);
		return socket;
	}

	@Override
	public ServerSocket createSocket(int port, int backlog,
	                                 InetAddress ifAddress)
			throws IOException {
		init();
		ServerSocket socket = sslProxy.createServerSocket(port, backlog,
				ifAddress);
		initServerSocket(socket);
		return socket;
	}

	@Override
	public Socket acceptSocket(ServerSocket socket)
			throws IOException {
		SSLSocket asock = null;
		try {
			asock = (SSLSocket) socket.accept();
		} catch (SSLException e) {
			throw new SocketException("SSL handshake error" + e.toString());
		}
		return asock;
	}

	@Override
	public void handshake(Socket sock) throws IOException {
		// We do getSession instead of startHandshake() so we can call this multiple times
		SSLSession session = ((SSLSocket) sock).getSession();
		if (session.getCipherSuite().equals("SSL_NULL_WITH_NULL_NULL"))
			throw new IOException("SSL handshake failed. Ciper suite in SSL Session is SSL_NULL_WITH_NULL_NULL");

		if (!allowUnsafeLegacyRenegotiation && !rfc5746Supported) {
			// Prevent further handshakes by removing all cipher suites
			((SSLSocket) sock).setEnabledCipherSuites(new String[0]);
		}
	}

	@Override
	public String[] getEnableableCiphers(SSLContext context) {
		String requestedCiphersStr = endpoint.getCiphers();

		if (ALLOW_ALL_SUPPORTED_CIPHERS.equals(requestedCiphersStr)) {
			return context.getSupportedSSLParameters().getCipherSuites();
		}
		if ((requestedCiphersStr == null)
				|| (requestedCiphersStr.trim().length() == 0)) {
			return defaultServerCipherSuites;
		}

		List<String> requestedCiphers = new ArrayList<String>();
		for (String rc : requestedCiphersStr.split(",")) {
			final String cipher = rc.trim();
			if (cipher.length() > 0) {
				requestedCiphers.add(cipher);
			}
		}
		if (requestedCiphers.isEmpty()) {
			return defaultServerCipherSuites;
		}
		List<String> ciphers = new ArrayList<String>(requestedCiphers);
		String[] supportedCipherSuiteArray = context.getSupportedSSLParameters().getCipherSuites();
		// The IBM JRE will accept cipher suites names SSL_xxx or TLS_xxx but
		// only returns the SSL_xxx form for supported cipher suites. Therefore
		// need to filter the requested cipher suites using both forms with an
		// IBM JRE.
		List<String> supportedCipherSuiteList;
		if (JreVendor.IS_IBM_JVM) {
			supportedCipherSuiteList = new ArrayList<String>(supportedCipherSuiteArray.length * 2);
			for (String name : supportedCipherSuiteArray) {
				supportedCipherSuiteList.add(name);
				if (name.startsWith("SSL")) {
					supportedCipherSuiteList.add("TLS" + name.substring(3));
				}
			}
		} else {
			supportedCipherSuiteList = Arrays.asList(supportedCipherSuiteArray);
		}
		ciphers.retainAll(supportedCipherSuiteList);

		if (ciphers.isEmpty()) {
			log.warn(sm.getString("jsse.requested_ciphers_not_supported",
					requestedCiphersStr));
		}
		if (log.isDebugEnabled()) {
			log.debug(sm.getString("jsse.enableable_ciphers", ciphers));
			if (ciphers.size() != requestedCiphers.size()) {
				List<String> skipped = new ArrayList<String>(requestedCiphers);
				skipped.removeAll(ciphers);
				log.debug(sm.getString("jsse.unsupported_ciphers", skipped));
			}
		}

		return ciphers.toArray(new String[ciphers.size()]);
	}

	/*
	 * Gets the SSL server's keystore password.
	 */
	protected String getKeystorePassword() {
		String keystorePass = endpoint.getKeystorePass();
		if (keystorePass == null) {
			keystorePass = endpoint.getKeyPass();
		}
		if (keystorePass == null) {
			keystorePass = DEFAULT_KEY_PASS;
		}
		return keystorePass;
	}

	/*
	 * Gets the SSL server's keystore.
	 */
	protected KeyStore getKeystore(String type, String provider, String pass)
			throws IOException {

		String keystoreFile = endpoint.getKeystoreFile();
		if (keystoreFile == null)
			keystoreFile = defaultKeystoreFile;

		return getStore(type, provider, keystoreFile, pass);
	}

	/*
	 * Gets the SSL server's truststore.
	 */
	protected KeyStore getTrustStore(String keystoreType,
	                                 String keystoreProvider) throws IOException {
		KeyStore trustStore = null;

		String truststoreFile = endpoint.getTruststoreFile();
		if (truststoreFile == null) {
			truststoreFile = System.getProperty("javax.net.ssl.trustStore");
		}
		if (log.isDebugEnabled()) {
			log.debug("Truststore = " + truststoreFile);
		}

		String truststorePassword = endpoint.getTruststorePass();
		if (truststorePassword == null) {
			truststorePassword =
					System.getProperty("javax.net.ssl.trustStorePassword");
		}
		if (log.isDebugEnabled()) {
			log.debug("TrustPass = " + truststorePassword);
		}

		String truststoreType = endpoint.getTruststoreType();
		if (truststoreType == null) {
			truststoreType = System.getProperty("javax.net.ssl.trustStoreType");
		}
		if (truststoreType == null) {
			truststoreType = keystoreType;
		}
		if (log.isDebugEnabled()) {
			log.debug("trustType = " + truststoreType);
		}

		String truststoreProvider = endpoint.getTruststoreProvider();
		if (truststoreProvider == null) {
			truststoreProvider =
					System.getProperty("javax.net.ssl.trustStoreProvider");
		}
		if (truststoreProvider == null) {
			truststoreProvider = keystoreProvider;
		}
		if (log.isDebugEnabled()) {
			log.debug("trustProvider = " + truststoreProvider);
		}

		if (truststoreFile != null) {
			try {
				trustStore = getStore(truststoreType, truststoreProvider,
						truststoreFile, truststorePassword);
			} catch (IOException ioe) {
				Throwable cause = ioe.getCause();
				if (cause instanceof UnrecoverableKeyException) {
					// Log a warning we had a password issue
					log.warn(sm.getString("jsse.invalid_truststore_password"),
							cause);
					// Re-try
					trustStore = getStore(truststoreType, truststoreProvider,
							truststoreFile, null);
				} else {
					// Something else went wrong - re-throw
					throw ioe;
				}
			}
		}

		return trustStore;
	}

	/*
	 * Gets the key- or truststore with the specified type, path, and password.
	 */
	private KeyStore getStore(String type, String provider, String path,
	                          String pass) throws IOException {

		KeyStore ks = null;
		InputStream istream = null;
		try {
			if (provider == null) {
				ks = KeyStore.getInstance(type);
			} else {
				ks = KeyStore.getInstance(type, provider);
			}
			if (!("PKCS11".equalsIgnoreCase(type) ||
					"".equalsIgnoreCase(path))) {
				istream = ConfigFileLoader.getInputStream(path);
			}

			char[] storePass = null;
			if (pass != null && !"".equals(pass)) {
				storePass = pass.toCharArray();
			}
			ks.load(istream, storePass);
		} catch (FileNotFoundException fnfe) {
			log.error(sm.getString("jsse.keystore_load_failed", type, path,
					fnfe.getMessage()), fnfe);
			throw fnfe;
		} catch (IOException ioe) {
			// May be expected when working with a trust store
			// Re-throw. Caller will catch and log as required
			throw ioe;
		} catch (Exception ex) {
			String msg = sm.getString("jsse.keystore_load_failed", type, path,
					ex.getMessage());
			log.error(msg, ex);
			throw new IOException(msg);
		} finally {
			if (istream != null) {
				try {
					istream.close();
				} catch (IOException ioe) {
					// Do nothing
				}
			}
		}

		return ks;
	}

	/**
	 * Reads the keystore and initializes the SSL socket factory.
	 */
	void init() throws IOException {
		try {

			String clientAuthStr = endpoint.getClientAuth();
			if ("true".equalsIgnoreCase(clientAuthStr) ||
					"yes".equalsIgnoreCase(clientAuthStr)) {
				requireClientAuth = true;
			} else if ("want".equalsIgnoreCase(clientAuthStr)) {
				wantClientAuth = true;
			}

			SSLContext context = createSSLContext();
			context.init(getKeyManagers(), getTrustManagers(), null);

			// Configure SSL session cache
			SSLSessionContext sessionContext =
					context.getServerSessionContext();
			if (sessionContext != null) {
				configureSessionContext(sessionContext);
			}

			// create proxy
			sslProxy = context.getServerSocketFactory();

			// Determine which cipher suites to enable
			enabledCiphers = getEnableableCiphers(context);
			enabledProtocols = getEnableableProtocols(context);

			allowUnsafeLegacyRenegotiation = "true".equals(
					endpoint.getAllowUnsafeLegacyRenegotiation());

			// Check the SSL config is OK
			checkConfig();

		} catch (Exception e) {
			if (e instanceof IOException)
				throw (IOException) e;
			throw new IOException(e.getMessage(), e);
		}
	}

	@Override
	public SSLContext createSSLContext() throws Exception {

		// SSL protocol variant (e.g., TLS, SSL v3, etc.)
		String protocol = endpoint.getSslProtocol();
		if (protocol == null) {
			protocol = defaultProtocol;
		}

		SSLContext context = SSLContext.getInstance(protocol);

		return context;
	}

	@Override
	public KeyManager[] getKeyManagers() throws Exception {
		String keystoreType = endpoint.getKeystoreType();
		if (keystoreType == null) {
			keystoreType = defaultKeystoreType;
		}

		String algorithm = endpoint.getAlgorithm();
		if (algorithm == null) {
			algorithm = KeyManagerFactory.getDefaultAlgorithm();
		}

		return getKeyManagers(keystoreType, endpoint.getKeystoreProvider(),
				algorithm, endpoint.getKeyAlias());
	}

	@Override
	public TrustManager[] getTrustManagers() throws Exception {
		String truststoreType = endpoint.getTruststoreType();
		if (truststoreType == null) {
			truststoreType = System.getProperty("javax.net.ssl.trustStoreType");
		}
		if (truststoreType == null) {
			truststoreType = endpoint.getKeystoreType();
		}
		if (truststoreType == null) {
			truststoreType = defaultKeystoreType;
		}

		String algorithm = endpoint.getTruststoreAlgorithm();
		if (algorithm == null) {
			algorithm = TrustManagerFactory.getDefaultAlgorithm();
		}

		return getTrustManagers(truststoreType, endpoint.getKeystoreProvider(),
				algorithm);
	}

	@Override
	public void configureSessionContext(SSLSessionContext sslSessionContext) {
		int sessionCacheSize;
		if (endpoint.getSessionCacheSize() != null) {
			sessionCacheSize = Integer.parseInt(
					endpoint.getSessionCacheSize());
		} else {
			sessionCacheSize = defaultSessionCacheSize;
		}

		int sessionTimeout;
		if (endpoint.getSessionTimeout() != null) {
			sessionTimeout = Integer.parseInt(endpoint.getSessionTimeout());
		} else {
			sessionTimeout = defaultSessionTimeout;
		}

		sslSessionContext.setSessionCacheSize(sessionCacheSize);
		sslSessionContext.setSessionTimeout(sessionTimeout);
	}

	/**
	 * Gets the initialized key managers.
	 */
	protected KeyManager[] getKeyManagers(String keystoreType,
	                                      String keystoreProvider,
	                                      String algorithm,
	                                      String keyAlias)
			throws Exception {

		KeyManager[] kms = null;

		String keystorePass = getKeystorePassword();

		KeyStore ks = getKeystore(keystoreType, keystoreProvider, keystorePass);
		if (keyAlias != null && !ks.isKeyEntry(keyAlias)) {
			throw new IOException(
					sm.getString("jsse.alias_no_key_entry", keyAlias));
		}

		KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm);
		String keyPass = endpoint.getKeyPass();
		if (keyPass == null) {
			keyPass = keystorePass;
		}
		kmf.init(ks, keyPass.toCharArray());

		kms = kmf.getKeyManagers();
		if (keyAlias != null) {
			String alias = keyAlias;
			if (JSSESocketFactory.defaultKeystoreType.equals(keystoreType)) {
				alias = alias.toLowerCase(Locale.ENGLISH);
			}
			for (int i = 0; i < kms.length; i++) {
				kms[i] = new JSSEKeyManager((X509KeyManager) kms[i], alias);
			}
		}

		return kms;
	}

	/**
	 * Gets the initialized trust managers.
	 */
	protected TrustManager[] getTrustManagers(String keystoreType,
	                                          String keystoreProvider, String algorithm)
			throws Exception {
		String crlf = endpoint.getCrlFile();

		String className = endpoint.getTrustManagerClassName();
		if (className != null && className.length() > 0) {
			ClassLoader classLoader = getClass().getClassLoader();
			Class<?> clazz = classLoader.loadClass(className);
			if (!(TrustManager.class.isAssignableFrom(clazz))) {
				throw new InstantiationException(sm.getString(
						"jsse.invalidTrustManagerClassName", className));
			}
			Object trustManagerObject = clazz.newInstance();
			TrustManager trustManager = (TrustManager) trustManagerObject;
			return new TrustManager[]{trustManager};
		}

		TrustManager[] tms = null;

		KeyStore trustStore = getTrustStore(keystoreType, keystoreProvider);
		if (trustStore != null || endpoint.getTrustManagerClassName() != null) {
			if (crlf == null) {
				TrustManagerFactory tmf =
						TrustManagerFactory.getInstance(algorithm);
				tmf.init(trustStore);
				tms = tmf.getTrustManagers();
			} else {
				TrustManagerFactory tmf =
						TrustManagerFactory.getInstance(algorithm);
				CertPathParameters params =
						getParameters(algorithm, crlf, trustStore);
				ManagerFactoryParameters mfp =
						new CertPathTrustManagerParameters(params);
				tmf.init(mfp);
				tms = tmf.getTrustManagers();
			}
		}

		return tms;
	}

	/**
	 * Return the initialization parameters for the TrustManager.
	 * Currently, only the default <code>PKIX</code> is supported.
	 *
	 * @param algorithm  The algorithm to get parameters for.
	 * @param crlf       The path to the CRL file.
	 * @param trustStore The configured TrustStore.
	 * @return The parameters including the CRLs and TrustStore.
	 */
	protected CertPathParameters getParameters(String algorithm,
	                                           String crlf,
	                                           KeyStore trustStore)
			throws Exception {
		CertPathParameters params = null;
		if ("PKIX".equalsIgnoreCase(algorithm)) {
			PKIXBuilderParameters xparams =
					new PKIXBuilderParameters(trustStore, new X509CertSelector());
			Collection<? extends CRL> crls = getCRLs(crlf);
			CertStoreParameters csp = new CollectionCertStoreParameters(crls);
			CertStore store = CertStore.getInstance("Collection", csp);
			xparams.addCertStore(store);
			xparams.setRevocationEnabled(true);
			String trustLength = endpoint.getTrustMaxCertLength();
			if (trustLength != null) {
				try {
					xparams.setMaxPathLength(Integer.parseInt(trustLength));
				} catch (Exception ex) {
					log.warn("Bad maxCertLength: " + trustLength);
				}
			}

			params = xparams;
		} else {
			throw new CRLException("CRLs not supported for type: " + algorithm);
		}
		return params;
	}

	/**
	 * Load the collection of CRLs.
	 */
	protected Collection<? extends CRL> getCRLs(String crlf)
			throws IOException, CRLException, CertificateException {

		Collection<? extends CRL> crls = null;
		InputStream is = null;
		try {
			CertificateFactory cf = CertificateFactory.getInstance("X.509");
			is = ConfigFileLoader.getInputStream(crlf);
			crls = cf.generateCRLs(is);
		} catch (IOException iex) {
			throw iex;
		} catch (CRLException crle) {
			throw crle;
		} catch (CertificateException ce) {
			throw ce;
		} finally {
			if (is != null) {
				try {
					is.close();
				} catch (Exception ex) {
					// Ignore
				}
			}
		}
		return crls;
	}

	@Override
	public String[] getEnableableProtocols(SSLContext context) {
		String[] requestedProtocols = endpoint.getSslEnabledProtocolsArray();
		if ((requestedProtocols == null) || (requestedProtocols.length == 0)) {
			return defaultServerProtocols;
		}

		List<String> protocols = new ArrayList<String>(
				Arrays.asList(requestedProtocols));
		protocols.retainAll(Arrays.asList(context.getSupportedSSLParameters()
				.getProtocols()));

		if (protocols.isEmpty()) {
			log.warn(sm.getString("jsse.requested_protocols_not_supported",
					Arrays.asList(requestedProtocols)));
		}
		if (log.isDebugEnabled()) {
			log.debug(sm.getString("jsse.enableable_protocols", protocols));
			if (protocols.size() != requestedProtocols.length) {
				List<String> skipped = new ArrayList<String>(
						Arrays.asList(requestedProtocols));
				skipped.removeAll(protocols);
				log.debug(sm.getString("jsse.unsupported_protocols", skipped));
			}
		}
		return protocols.toArray(new String[protocols.size()]);
	}

	/**
	 * Configure Client authentication for this version of JSSE.  The
	 * JSSE included in Java 1.4 supports the 'want' value.  Prior
	 * versions of JSSE will treat 'want' as 'false'.
	 *
	 * @param socket the SSLServerSocket
	 */
	protected void configureClientAuth(SSLServerSocket socket) {
		if (wantClientAuth) {
			socket.setWantClientAuth(wantClientAuth);
		} else {
			socket.setNeedClientAuth(requireClientAuth);
		}
	}

	/**
	 * Configures SSLEngine to honor cipher suites ordering based upon
	 * endpoint configuration.
	 *
	 * @throws InvalidAlgorithmParameterException If the runtime JVM doesn't
	 *                                            support this setting.
	 */
	protected void configureUseServerCipherSuitesOrder(SSLServerSocket socket) {
		String useServerCipherSuitesOrderStr = endpoint
				.getUseServerCipherSuitesOrder().trim();

		// Only use this feature if the user explicitly requested its use.
		if (!"".equals(useServerCipherSuitesOrderStr)) {
			boolean useServerCipherSuitesOrder =
					("true".equalsIgnoreCase(useServerCipherSuitesOrderStr)
							|| "yes".equalsIgnoreCase(useServerCipherSuitesOrderStr));
			JreCompat jreCompat = JreCompat.getInstance();
			jreCompat.setUseServerCipherSuitesOrder(socket, useServerCipherSuitesOrder);
		}
	}

	/**
	 * Configures the given SSL server socket with the requested cipher suites,
	 * protocol versions, and need for client authentication
	 */
	private void initServerSocket(ServerSocket ssocket) {

		SSLServerSocket socket = (SSLServerSocket) ssocket;

		socket.setEnabledCipherSuites(enabledCiphers);
		socket.setEnabledProtocols(enabledProtocols);

		// we don't know if client auth is needed -
		// after parsing the request we may re-handshake
		configureClientAuth(socket);
		configureUseServerCipherSuitesOrder(socket);
	}

	/**
	 * Checks that the certificate is compatible with the enabled cipher suites.
	 * If we don't check now, the JIoEndpoint can enter a nasty logging loop.
	 * See bug 45528.
	 */
	private void checkConfig() throws IOException {
		// Create an unbound server socket
		ServerSocket socket = sslProxy.createServerSocket();
		initServerSocket(socket);

		try {
			// Set the timeout to 1ms as all we care about is if it throws an
			// SSLException on accept.
			socket.setSoTimeout(1);

			socket.accept();
			// Will never get here - no client can connect to an unbound port
		} catch (SSLException ssle) {
			// SSL configuration is invalid. Possibly cert doesn't match ciphers
			IOException ioe = new IOException(sm.getString(
					"jsse.invalid_ssl_conf", ssle.getMessage()));
			ioe.initCause(ssle);
			throw ioe;
		} catch (Exception e) {
	        /*
             * Possible ways of getting here
             * socket.accept() throws a SecurityException
             * socket.setSoTimeout() throws a SocketException
             * socket.accept() throws some other exception (after a JDK change)
             *      In these cases the test won't work so carry on - essentially
             *      the behaviour before this patch
             * socket.accept() throws a SocketTimeoutException
             *      In this case all is well so carry on
             */
		} finally {
			// Should be open here but just in case
			if (!socket.isClosed()) {
				socket.close();
			}
		}

	}
}
